<?php
/**
 * Created by PhpStorm.
 * User: Inhere
 * Date: 2015/1/30
 * Use : 与 AbstractDataOperator 相同
 * File: TraitDataOperator.php
 */

namespace ulue\libs\datastorage;

use ulue\libs\datastorage\helpers\DataHelper;
use ulue\libs\traits\TraitUseAlias;

trait TraitDataOperator
{

    /**
     * 全部数据
     * 名 => 值
     * @var array
     */
    protected $box = [];

    /**
     * 设置数据($box)的键名(配置名)是否统一格式
     * DataOption::CASE_RAW 不做任何改变
     * DataOption::CASE_LOWER 设置全部小写
     * DataOption::CASE_UPPER 设置全部大写
     * @var int
     */
    protected $keyCase = -1;

    /**
     * 检查节点名称，限制字符、 长度
     * @var array
     */
    protected $checkPath = true;

    /**
     * 保存锁定的节点
     * @var array
     */
    protected $locked = [];

    /**
     * 允许给 $path 设置别名
     * @example (new Datastorage)->set('my','simon tony')->alias('me','my')->alias('i');
     */
    use TraitUseAlias;

    /**
     * 数据存储方式
     * @return string 'full' | 'simple'
     * @example (new Datastorage)->set('my.name','simon tony')
     * 1. 'full' 完整清晰的存储使用,
     *  会以点[.]拆分$path为子节点
     *  将会存储为:
     *   $this->box => array (
     *       'my.name' => '/static' ,   // 方便用 get('my.name') 取值
     *       'my'      => array (       // 此时 也可以取到 'my' 的值 get('my')，并且是个数组
     *           'name'     => '/static'
     *       )
     *   )
     * 2. 'simple' 简单存储使用,
     *  不会以点[.]拆分$path为子节点
     *  将会存储为:
     *      // 只可用 get('my.name') 取值，用 get('my') 将会返回 null
     *      $this->box => array ('my.name' => '/static' )
     */
    protected function storeMode()
    {
        return 'simple';
    }

    /**
     * 设置值,不存在的节点会默认创建
     * @param $path
     * @param $value
     * @return mixed
     * @throws DatastorageException
     */
    public function def($path, $value)
    {
        return $this->set($path, $value,true);
    }

    /**
     * def() alias method 设置值,不存在的节点会默认创建
     * @param $path
     * @param $value
     * @return mixed
     * @throws DatastorageException
     */
    public function add($path, $value)
    {
        return $this->def($path, $value);
    }

    /**
     * 设置值
     * @example set('db.cache.enabled','1')
     * @param string|array $path
     * @param $value
     * @param bool $createNode 不存在指向节点时，是否创建新的节点
     * @throws DatastorageException
     * @return object $this 用于支持链式操作
     */
    public function set($path, $value, $createNode=false)
    {
        $temp  = $this->box;

        // 检查 $path 是否合法
        $path = $this->_checkInputPath($path);

        // 用于设置别名时可省略第二个参数， (new Datastorage)->set('my','simon tony')->alias('me','my')->alias('i');
        $this->id = $path;

        if ( $this->isLocked($path) ) {
            throw new DatastorageException("[{$path}] 节点数据已被锁定！无法修改。");
        }

        // 如果没有分隔符 '.'  或者 @property $storeMode='simple', 设置值到此结束
        if ( strpos($path,'.')===false || $this->storeMode() == 'simple') {
            $this->box[$path] = $value;

            return $this;
        }

        //////////////////////////// $this->storeMode() == 'full' /////////////////////////////////

        /**
         * 有子节点(多维数组)，以点'.'连接的 $path
         * @example Ulue::$app->set('db.cache.enabled',true)
         */
        $result      = $this->_searchChildNode($path,$temp);
        $find        = $result['find'];
        $nodes       = $result['nodes'];
        $first       = array_shift($nodes); //存储根节点名

        // 存储已查找到的节点
        $lookedNodes = $result['looked'];

        // path node 全部被找到
        if ( $find === true ) {
            $createdFirstNode = $this->_createNode($nodes, $value);
            $array            = DataHelper::mergeArrays($temp[$first], $createdFirstNode);

        }else {
            // 有节点不存在，也没有设置允许新创建。抛出异常
            if (!$createNode) {
                $lookedNodeStr = !$lookedNodes ? $first : implode('.',$lookedNodes);
                throw new DatastorageException(sprintf('数据中没有存储节点%s,不能设置对应值！', $lookedNodeStr), 'conf');
            }

            //完全不存在，创建创建新的第一级节点数据
            if (!$lookedNodes) {
                $array              = $this->_createNode($nodes, $value);
                $this->box[$first]  =  $array;

                return $this;
            }

            //部分父级存在，创建剩下的的层级，设置值
            $restNodes      = array_diff($nodes, $lookedNodes);
            $restNodeData   = $this->_createNode($restNodes, $value);
            $array          = $this->_createNode($lookedNodes, $restNodeData);
            $array          = DataHelper::mergeArrays($temp[$first], $array[$first]);
        }

        $this->box[$first]     =  $array;

        unset($temp, $path, $nodes, $nodes, $n);

        return $this;
    }

    public function mset(array $data)
    {
        foreach($data as $path=>$value) {
            $this->set($path, $value);
        }

        return $this;
    }

    /**
     * 搜索子节点，整理数据
     * @param $path
     * @param $temp
     * @return array
     */
    private function _searchChildNode($path, $temp)
    {
        if(!strpos($path,'.')) {
            return false;
        }

        $nodes       = $tempNodes  = array_values(array_filter(explode('.', $path), 'strlen'));
        $lookedNodes = [];
        $find        = true;

        // 循环查找子节点是否存在
        for ($i=0,$n = count($nodes); $i < $n; $i++) {
            $key = array_shift($nodes);

            if ( !isset($temp[$key]) ) {
                $find = false;
                break;
            }

            $lookedNodes[]  = $key;
            $temp           = $temp[$key];
        }

        return array('find'=>$find,'looked'=>$lookedNodes,'nodes'=>$tempNodes,'value'=>$temp);
    }


    /**
     * 获取值
     * @note 当配置值是闭包(匿名函数)时会自动调用，不支持传入参数
     * @param $path
     * @param null $default 若没有此配置值，则返回 $default
     * @return mixed
     */
    public function get($path, $default=null)
    {
        $temp = $this->box;
        $path = $this->_checkInputPath($path);
        $path = $this->resolveAlias($path);

        if ( isset( $temp[$path] ) ) {
            $value = $temp[$path];

            return ($value instanceof \Closure) ? $value() : $value;
        }

        /**
         * 多维数组，以点'.'连接的 $path直接获取。
         * @example Ulue::$app->get('db.username')
         */
        $nodes  = explode('.',$path);
        $n      = count($nodes);

        for ($i=0; $i < $n; $i++) {
            $key = $nodes[$i];

            if ( isset($temp[$key]) ) {
                $temp = $temp[$key];
            } else {
                return $default;
            }
        }

        /**
         * @todo 当前 $path 是否存储起来？存： 便于下次获取，但是set()更新同一个$path值时，
         * @todo 得更新两个[$this->box[$path] 和 $path指向的真实子节点值]
         * @todo 不存：获取时每次都得循环扫描子节点，增加计算
         */
        $this->box[$path]  =  $temp;

        unset($path,$nodes,$n);

        return ($temp instanceof \Closure) ? $temp() : $temp;
    }

    public function mget(array $names)
    {
        $data = [];

        foreach($names as $name) {
            $data[$name] = $this->get($name);
        }

        return $data;
    }


    public function exists($path)
    {
        $temp  = $this->box;

        //检查 $path 是否合法
        $path = $this->_checkInputPath($path);

        if ( isset( $temp[$path] ) ) {
            return true;
        }

        // 如果没有分隔符 '.'  判断结束
        if ( strpos($path,'.')===false ) {
            return false;
        }

        $result      = $this->_searchChildNode($path,$temp);

        return $result['find'] ? true : false;
    }

    /**
     * 锁定节点，不允许修改
     * @param $path
     * @param bool $type
     * @return $this
     */
    public function lock($path, $type = true)
    {
        if ( (bool) $type ) {
            $this->locked[] = $path;
        } else {
            $this->unlock($path);
        }

        return $this;
    }

    public function unlock($path)
    {
        if ( in_array($path, $this->locked) ) {
            foreach ($this->locked as $key=>$locked) {
                if ($locked == $path)
                {
                    unset($this->locked[$key]);
                }
            }
        }

        return $this;
    }

    public function isLocked($path)
    {
        return in_array($path, $this->locked) ? true:false;
    }

    public function clear()
    {
        $this->box = [];

        return $this;
    }

    /**
     * @return array
     */
    public function getBox()
    {
        return $this->box;
    }

    public function getAll()
    {
        return $this->box;
    }

    /**
     * 检查节点路径是否合法
     * @param $path
     * @throws \DInvalidArgumentException
     * @return string
     */
    protected function _checkInputPath($path)
    {
        if ( empty($path) ) {
            throw new \DInvalidArgumentException( '必须传入配置项名称！' );
        }

        if (!$this->checkPath) {
            return trim($path);
        }

        if ( !is_string($path) || strlen($path)>30 ) {
            throw new \DInvalidArgumentException( '配置名称只能是不超过30个字符的字符串！');
        }

        //去处空白和前后的'.'
        $path = trim( str_replace(' ','',$path), '.');

        if ( !preg_match("/^\w{1,20}(?:\.\w{1,20})*$/i", $path) ) {
            throw new \DInvalidArgumentException( sprintf('配置名称[%s]是无效的字符串！', $path));
        }

        if ( $this->keyCase ===  DataOption::CASE_LOWER ) {
            return strtolower($path);
        }

        if ( $this->keyCase === DataOption::CASE_UPPER ) {
            return strtoupper($path);
        }

        return $path;
    }

    /**
     * $nodes = [
     *    0 => "db"
     *    1 => "cache"
     *    2 => "enabled"
     *   ]
     * $value = 1
     *==>
     * $new = [
     *   'db'=> [
     *       'cache'=> [
     *           'enabled'=>'1'
     *           ]
     *       ]
     *   ]
     * @param array $nodes
     * @param $value
     * @return array
     */
    private function _createNode(array $nodes, $value)
    {
        if (!$nodes) {
            return $value;
        }

        $new        = [];
        $node       = array_shift($nodes);
        $new[$node] = $nodes ? $this->_createNode($nodes,$value) : $value ;

        return $new;
    }

    /**
     * Defined by IteratorAggregate interface
     * Returns an iterator for this object, for use with foreach
     * @return \ArrayIterator
     */
    public function getIterator()
    {
        return new \ArrayIterator($this->box);
    }


    /**
     * Checks whether an offset exists in the iterator.
     * @param   mixed  $offset  The array offset.
     * @return  boolean  True if the offset exists, false otherwise.
     */
    public function offsetExists($offset)
    {
        return (boolean) ($this->get($offset) !== null);
    }

    /**
     * Gets an offset in the iterator.
     * @param   mixed  $offset  The array offset.
     * @return  mixed  The array value if it exists, null otherwise.
     */
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * Sets an offset in the iterator.
     * @param   mixed  $offset  The array offset.
     * @param   mixed  $value   The array value.
     * @return  void
     */
    public function offsetSet($offset, $value)
    {
        $this->set($offset, $value, true);
    }

    /**
     * Unset an offset in the iterator.
     * @param   mixed  $offset  The array offset.
     * @return  void
     */
    public function offsetUnset($offset)
    {
        $this->set($offset, null);
    }

    public function __set($name, $value)
    {
        return $this->set($name, $value);
    }

    public function __get($name)
    {
        if ( isset($this->box[$name]) ) {
            return $this->box[$name];
        }

        throw new \DUnknownCalledException(sprintf('获取不存在的属性 %s ！',$name));
    }

}// end class AbstractStoreData